// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.embedding.android;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;

import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
import io.flutter.plugin.editing.TextInputPlugin;

public class AndroidKeyProcessor {
  @NonNull
  private final KeyEventChannel keyEventChannel;
  @NonNull
  private final TextInputPlugin textInputPlugin;
  private int combiningCharacter;

  public AndroidKeyProcessor(@NonNull KeyEventChannel keyEventChannel, @NonNull TextInputPlugin textInputPlugin) {
    this.keyEventChannel = keyEventChannel;
    this.textInputPlugin = textInputPlugin;
  }

  public void onKeyUp(@NonNull KeyEvent keyEvent) {
    Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
    keyEventChannel.keyUp(
        new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter)
    );
  }

  public void onKeyDown(@NonNull KeyEvent keyEvent) {
    if (textInputPlugin.getLastInputConnection() != null
        && textInputPlugin.getInputMethodManager().isAcceptingText()) {
      textInputPlugin.getLastInputConnection().sendKeyEvent(keyEvent);
    }

    Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
    keyEventChannel.keyDown(
        new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter)
    );
  }

  /**
   * Applies the given Unicode character in {@code newCharacterCodePoint} to a previously
   * entered Unicode combining character and returns the combination of these characters
   * if a combination exists.
   * <p>
   * This method mutates {@link #combiningCharacter} over time to combine characters.
   * <p>
   * One of the following things happens in this method:
   * <ul>
   *   <li>If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
   *   is not a combining character, then {@code newCharacterCodePoint} is returned.</li>
   *   <li>If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
   *   is a combining character, then {@code newCharacterCodePoint} is saved as the
   *   {@link #combiningCharacter} and null is returned.</li>
   *   <li>If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
   *   is also a combining character, then the {@code newCharacterCodePoint} is combined with
   *   the existing {@link #combiningCharacter} and null is returned.</li>
   *   <li>If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
   *    is not a combining character, then the {@link #combiningCharacter} is applied to the
   *    regular {@code newCharacterCodePoint} and the resulting complex character is returned. The
   *    {@link #combiningCharacter} is cleared.</li>
   * </ul>
   * <p>
   * The following reference explains the concept of a "combining character":
   * https://en.wikipedia.org/wiki/Combining_character
   */
  @Nullable
  private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) {
    if (newCharacterCodePoint == 0) {
      return null;
    }

    Character complexCharacter = (char) newCharacterCodePoint;
    boolean isNewCodePointACombiningCharacter = (newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0;
    if (isNewCodePointACombiningCharacter) {
      // If a combining character was entered before, combine this one with that one.
      int plainCodePoint = newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT_MASK;
      if (combiningCharacter != 0) {
        combiningCharacter = KeyCharacterMap.getDeadChar(combiningCharacter, plainCodePoint);
      } else {
        combiningCharacter = plainCodePoint;
      }
    } else {
      // The new character is a regular character. Apply combiningCharacter to it, if it exists.
      if (combiningCharacter != 0) {
        int combinedChar = KeyCharacterMap.getDeadChar(combiningCharacter, newCharacterCodePoint);
        if (combinedChar > 0) {
          complexCharacter = (char) combinedChar;
        }
        combiningCharacter = 0;
      }
    }

    return complexCharacter;
  }
}
